Java_Concurrency_In_Practise笔记(1)-Thread safety
使用多线程编程时会遇到的问题
- 数据一致性问题: nothing bad ever happen
- Liveness Failure: something good eventually happens, deadlock, starvation, livelock
- 性能问题:多线程时,CPU需要在不同的上下文之间进行切换,线程间的同步操作等也会耗费一些CPU时间
数据一致性
编写线程安全的代码的本质是:管理好对那些可共享,可变状态的读写操作
任何情况下,如果多个线程会同时对某个状态访问,并且其中至少一个线程会对状态进行修改,那么它们就必须通过某些机制来保证数据的一致性,这也是判断某个对象是否是线程安全的依据
1. Race condition
出现数据一致性的情况基本上都具备了上述的两个条件:同时有多个线程对对象的状态进行访问,并且其中至少一个线程会对状态进行修改
竞争条件指的是两个或多个线程读写某些共享数据,而最后的结果取决于线程的精确执行时序,这种情况在多线程中非常常见。
check-and-act:执行的操作取决于某个观察到的先决条件,但当开始act(act并不一定是写操作,也可以是读操作)的时候,之前观察到的先决条件已经失效
123e.g. 检查到文件系统中没有文件X,于是开始创建X,但是当开始创建X时,另一个线程已经在文件系统中创建了Xe.g. lazy initialization: 单例模式read-modify-write: 由于这种操作的结果依赖于之前的操作,当写操作开始的时候,可能之前读取到的状态值已经发生变化
1e.g. i++
2. 解决办法
解决数据的一致性问题也可以通过两个方面来进行:控制同一时间访问状态的线程数量,或者让状态变为只读变量,甚至可以将类实现为无状态类,无状态类天生就是线程安全的
在JAVA中,同步的机制包括:synchronized, volatile,原子化变量,显式锁(这些方法本质上都是限制了同一时间只能有一个线程能操作对象的状态)
2.1 原子化变量
对原子化变量的所有操作都是原子化的,从内存的角度来看,基本上同操作volatile变量是一样的(对volatile变量的写操作与随后对该变量的读操作之间有happen-before关系),因此这些操作都是线程安全的
2.2 synchronized
synchronized由两个部分组成,一个是获取到的锁对象,另一个是锁的代码块
synchronized方法是synchronized代码块的一种简略表达,它包含方法中所有的代码块,锁的对象是调用该方法的那个对象;如果是对static方法加synchronized关键字,那么锁的对象的是该方法所在的类
JAVA中的每个对象都有内置锁,当线程进入synchronized区域时,自动得到某个对象(synchronized后的对象)的内置锁,并在退出synchronized代码块时自动释放该对象锁,不管这种退出是正常的执行结束还是由于代码出现异常
2.3 重入锁
当一个线程试图获取某个锁时,如果这个锁已经被其它的线程所占有,那么该线程会阻塞,但如果拥有这个锁的线程就是它本身,那么它可以重复地获取锁,这种机制被称为是重入锁,这种重入锁是通过记录拥有锁的线程对象以及获取锁的次数来实现的。
2.4 利用锁实现状态的统一
这里的状态并不一定是指某个变量,也可能是几个变量组合成的状态,在读写这些状态的时候,需要通过锁机制(不管是synchronized关键字还是Lock机制)保证同一时间只有一个线程在对它们进行操作
另外,对某个类中所有的方法加上synchronized关键字并不能保证这个类的使用中就是线程安全的,如
|
|
这是个典型的check-and-act代码;在这段代码中,即使这两个方法都是原子操作,也不能保证这两个操作是原子的。